﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Sttplay.Transmission.TCP
{
    public class TCPClient : TCPReceiver
    {
        private enum ConnectState
        {
            None,
            Success,
            Failed
        }

        private readonly Mutex mux = new Mutex();
        private Thread connectThread;
        private EndPoint remote;
        private int timeout;
        private ConnectState connectState = ConnectState.None;
        private string connectLog;
        private bool timerThreadExit = false;
        private bool notify = false;

        public bool Connected { get; private set; }
        public bool Removed { get; private set; }

        public event Action<string> onErrorLogEvent;
        public event Action onConnectSuccessedEvent;
        public event Action<string> onConnectFailedEvent;
        public event Action<byte[]> onRecvEvent;
        public event Action onDisconnectEvent;
        private Action<TCPClient, byte[]> proxyRecvCallback;
        private bool isProxy = false;

        public TCPClient(bool isPacket = false)
        {
            base.isPacket = isPacket;
            recvCallback = OnRecv;
            errorLogCallback = OnErrorLog;
        }

        public TCPClient(Socket s, Action<TCPClient, byte[]> recvCb, Action<string> errorCb, bool isPacket) : base(s)
        {
            Removed = false;
            Connected = true;
            base.socket = s;
            base.isPacket = isPacket;
            recvCallback = OnRecv;
            errorLogCallback = errorCb;
            proxyRecvCallback = recvCb;
            isProxy = true;
        }

        public void AsyncConnect(IPAddress ip, int port, int timeout = int.MaxValue)
        {
            if (proxyRecvCallback != null)
            {
                OnConnectFailed("The current socket does not allow the connection operation");
                return;
            }
            Connected = false;
            Disconnect();
            CheckConnectState();
            connectLog = null;
            this.remote = new IPEndPoint(ip, port);
            this.timeout = timeout;
            timerThreadExit = false;
            connectThread = new Thread(Connect);
            connectThread.Start();
        }

        public void Connect(IPAddress ip, int port, int timeout = int.MaxValue)
        {
            if (proxyRecvCallback != null) return;
            AsyncConnect(ip, port, timeout);
            if (connectThread != null)
                connectThread.Join();
        }

        private void Connect()
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            Thread thread = new Thread(TimerThread);
            thread.Start();
            try
            {
                mux.WaitOne();
                connectState = ConnectState.None;
                mux.ReleaseMutex();
                socket.Connect(remote);
                mux.WaitOne();
                connectState = ConnectState.Success;
                mux.ReleaseMutex();
            }
            catch (Exception ex)
            {
                mux.WaitOne();
                connectState = ConnectState.Failed;
                if (string.IsNullOrEmpty(connectLog))
                    connectLog = ex.ToString();
                mux.ReleaseMutex();
            }
            timerThreadExit = true;
            thread.Join();
            notify = true;
        }

        private void TimerThread()
        {
            long time = GetTimestamp();
            while (!timerThreadExit && GetTimestamp() - time < timeout)
                Thread.Sleep(10);

            mux.WaitOne();
            if (connectState == ConnectState.None)
            {
                if (GetTimestamp() - time > timeout)
                {
                    if (string.IsNullOrEmpty(connectLog))
                        connectLog = "connect timeout";
                }
                Connected = false;
                if (socket != null)
                    socket.Close();
                connectState = ConnectState.Failed;
            }
            mux.ReleaseMutex();
        }

        private static long GetTimestamp()
        {
            return (DateTime.UtcNow.Ticks - new DateTime(1970, 1, 1, 0, 0, 0, 0).Ticks) / 10000;
        }

        public void Disconnect()
        {
            timerThreadExit = true;
            if (socket != null)
            {
                try
                {
                    Removed = true;
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();
                    socket.Dispose();
                }
                catch
                { }
            }
            if (connectThread != null)
                connectThread.Join();
            connectThread = null;
            socket = null;
            Connected = false;
            Reset();
        }


        public override bool Send(byte[] buffer, int offset, int size)
        {
            if (socket == null || !Connected)
                return false;
            if (!base.Send(buffer, offset, size))
            {
                Disconnect();
                OnDisconnect();
            }
            return true;
        }

        private void CheckConnectState()
        {
            if (notify)
            {
                if (connectState == ConnectState.Success)
                    OnConnectSuccessed();
                else
                    OnConnectFailed(connectLog);
                connectState = ConnectState.None;
                notify = false;
            }
        }

        public override void Update()
        {
            base.Update();
            if (isProxy) return;
            CheckConnectState();
            while (socket != null && Connected)
            {
                List<Socket> temp = new List<Socket>();
                temp.Add(socket);
                try
                {
                    Socket.Select(temp, null, null, 0);
                }
                catch (Exception ex)
                {
                    Disconnect();
                    OnDisconnect();
                    OnErrorLog(ex.ToString());
                    continue;
                }
                if (temp.Count <= 0)
                    break;

                int len = 0;
                SocketError error = Receive(ref len);
                if (len <= 0 || error != SocketError.Success)
                {
                    if (error != SocketError.Success)
                        OnErrorLog("recv error:" + error);
                    Disconnect();
                    OnDisconnect();
                }
            }
        }

        private void OnErrorLog(string error)
        {
            if (onErrorLogEvent != null)
                onErrorLogEvent(error);
            if (errorLogCallback != null && errorLogCallback != OnErrorLog)
                errorLogCallback(error);
        }

        private void OnConnectFailed(string error)
        {
            Connected = false;
            Disconnect();
            if (onConnectFailedEvent != null)
                onConnectFailedEvent(error);
        }

        private void OnConnectSuccessed()
        {
            Connected = true;
            if (onConnectSuccessedEvent != null)
                onConnectSuccessedEvent();
        }

        private void OnDisconnect()
        {
            if (onDisconnectEvent != null)
                onDisconnectEvent();
        }

        private void OnRecv(byte[] data)
        {
            if (onRecvEvent != null)
                onRecvEvent(data);
            if (proxyRecvCallback != null)
                proxyRecvCallback(this, data);
        }
    }


}
